using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using ActiveSharp.Inspection;
using ActiveSharp.PropertyMapping;
using NUnit.Framework;

namespace ActiveSharp.Tests
{
    /// <summary>
    /// Additional tests to do with cases where a method is called on a struct field (something
    /// which confused the original parsing algorithm)
    /// </summary>
    [TestFixture]
    public class FieldToPropertyMapAdditionalTests
    {
        public class BaseClass
        {
            protected void SetValue<T>(ref T field, T value)
            {
                field = value;
            }
        }

        public class TestClass : BaseClass
        {
            private int? _someInt;
            private string _someString;
            private TestStruct _someStruct;

            public int? SomeInt
            {
                get { return _someInt; }
                set { SetValue(ref _someInt, value); }

            }

            public string SomeString
            {
                get { return _someString;  }
                set { SetValue(ref _someString, value); }
            }

            /* todo 
            public TestStruct SomeStruct
            {
                get { return _someStruct;}                
                set { SetValue(ref _someStruct, value);}
            }
             * */

            public bool HasSomeInt
            {
                get { return _someInt.HasValue; }  // like the SetValue call, this produces a ldflda instruction in the MSIL, and we need to make sure it does not confuse us
            }

            public string SomeStringLowerCase
            {
                get { return _someString.ToLower();}  // same structure as HasSomeInt, but on a class not a struct
            }

            public int SomeIntHashCode
            {
                get { return _someInt.GetHashCode(); }  // like HasSomeInt, but a virtual method call, on a method inherited from object (resulting in use of the contrained. prefix in the generated MSIL
            }

            /* todo
            public string PropertyThatCallsAStructMethodWithAParameter
            {
                get { return _someStruct.MethodWithOneParam("Foo" + GetHashCode());  }
            }

            public bool PropertyThatCallsAVirtualMethodOnAStructWithAParameter
            {
                get { return _someStruct.Equals("Foo"); }
            }
             * */

        }

        public struct TestStruct
        {
            public string MethodWithOneParam(string s)
            {
                return s.ToLower();
            }
        }

        #region Verify that the behaviour, which has caused our bug, is indeed happening as we think it is 

        /// <summary>
        /// This tends to look like the ldflda that we look for to find our ref fields....
        /// </summary>
        [Test]
        public void LdfldaIsEmittedWhenCallingMethodOnStructField()
        {
            bool found = false;
            PropertyInfo property = typeof(TestClass).GetProperty("HasSomeInt");
            MethodInspector inspector = new MethodInspector(property.GetGetMethod(true));
            foreach (Instruction current in inspector.Instructions)
            {
                if (current.OpCode == OpCodes.Ldflda && // load a field for use "by ref"
                    current.Offset(-1).OpCode == OpCodes.Ldarg_0) // and the field belongs to "this" (which is always arg0)
                {
                    found = true;
                }
            }
            Assert.IsTrue(found);
        }

        [Test]
        public void LdfldaIsNotEmittedWhenCallingMethodOnClassField()
        {
            bool found = false;
            PropertyInfo property = typeof(TestClass).GetProperty("SomeStringLowerCase");
            MethodInspector inspector = new MethodInspector(property.GetGetMethod(true));
            foreach (Instruction current in inspector.Instructions)
            {
                if (current.OpCode == OpCodes.Ldflda && // load a field for use "by ref"
                    current.Offset(-1).OpCode == OpCodes.Ldarg_0) // and the field belongs to "this" (which is always arg0)
                {
                    found = true;
                }
            }
            Assert.IsFalse(found);
        }

        [Test]
        public void LdfldaIsFollowedByCallToStructMethodWhenCallingMethodOnStructField()
        {
            bool found = false;
            PropertyInfo property = typeof(TestClass).GetProperty("HasSomeInt");
            MethodInspector inspector = new MethodInspector(property.GetGetMethod(true));
            foreach (Instruction current in inspector.Instructions)
            {
                if (current.OpCode == OpCodes.Ldflda && // load a field for use "by ref"
                    current.Offset(-1).OpCode == OpCodes.Ldarg_0) // and the field belongs to "this" (which is always arg0)
                {
                    Instruction next = current.Offset(1);
                    MethodBase method = next.Operand.AsMethod;
                    Assert.IsTrue(method.DeclaringType.IsValueType); // it's a call on a struct
                    Assert.IsTrue(method.DeclaringType == current.Operand.AsField.FieldType); // the struct is of the field type

                    found = true;
                }
            }
            Assert.IsTrue(found);
        }

        /// <summary>
        /// See http://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.constrained.aspx
        /// </summary>
        [Test]
        public void CallToStructMethodHasConstrainedPrefixWhenMethodIsInheritedFromObject()
        {
            bool found = false;
            PropertyInfo property = typeof(TestClass).GetProperty("SomeIntHashCode");
            MethodInspector inspector = new MethodInspector(property.GetGetMethod(true));
            foreach (Instruction current in inspector.Instructions)
            {
                if (current.OpCode == OpCodes.Ldflda && // load a field for use "by ref"
                    current.Offset(-1).OpCode == OpCodes.Ldarg_0) // and the field belongs to "this" (which is always arg0)
                {
                    Instruction next = current.Offset(1);
                    Assert.IsTrue(next.OpCode == OpCodes.Constrained); 
                    Assert.IsTrue(next.Operand.AsType == typeof(Nullable<int>));

                    Assert.IsTrue(next.Offset(1).OpCode == OpCodes.Callvirt);

                    found = true;
                }
            }
            Assert.IsTrue(found);
        }

        [Test]
        public void LdfldaIsNotFollowedByCallToStructMethodWhenCallingMethodOnThis()
        {
            bool found = false;
            PropertyInfo property = typeof(TestClass).GetProperty("SomeInt");
            MethodInspector inspector = new MethodInspector(property.GetSetMethod(true));
            foreach (Instruction current in inspector.Instructions)
            {
                if (current.OpCode == OpCodes.Ldflda && // load a field for use "by ref"
                    current.Offset(-1).OpCode == OpCodes.Ldarg_0) // and the field belongs to "this" (which is always arg0)
                {
                    Instruction next = current.Offset(1);
                    Assert.IsTrue(next.OpCode != OpCodes.Call);  // it is pushing the second param

                    Instruction nextPlusOne = current.Offset(2); // so this is the call
                    MethodBase method = nextPlusOne.Operand.AsMethod;
                    Assert.IsFalse(method.DeclaringType.IsValueType); // it's NOT a call on a struct
                    Assert.IsFalse(method.DeclaringType == current.Operand.AsField.FieldType); // the method is not on the field

                    found = true;
                }
            }
            Assert.IsTrue(found);
        }
    
        #endregion

        #region Verify that our solution as worked

        [Test]
        public void CanMapFieldsToPropertiesEvenWhenFieldAlsoHasAMethodCalledOnItInAnotherProperty()
        {
            FieldToPropertyMap m = new FieldToPropertyMap(typeof(TestClass));
            Assert.AreSame(typeof(TestClass).GetProperty("SomeInt"), m["_someInt"]);
            Assert.AreSame(typeof(TestClass).GetProperty("SomeString"), m["_someString"]);
            //todo: Assert.AreSame(typeof(TestClass).GetProperty("SomeStruct"), m["_someStruct"]);
        }

        #endregion
    }
}
